/*
 * Copyright 2009 XueSong Guo.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package cn.webwheel.template.plain;

import java.util.List;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 模板解析器。
 */
public class PlainTemplateParser {

    private CharSequence string;
    private int stringIdx;

    private Stack<PlainElement> stack;

    private static Pattern patForTag = Pattern.compile("<");
    private static Pattern patForTagClose = Pattern.compile(">");
    private static Pattern patForCmtClose = Pattern.compile("-->");
    private static Pattern patForName = Pattern.compile("^\\s*[a-zA-Z_][a-zA-Z_0-9\\-:\\.]*");
    private static Pattern patForEqual = Pattern.compile("^\\s*=\\s*");
    private static Pattern patForCddClose = Pattern.compile("]]>");
    private static Pattern patForNotBlank = Pattern.compile("\\S");
    private static Pattern patForAttrValue = Pattern.compile("(^\"[^\"]*\")|(^'[^']*')");

    private StringBuilder text;

    public PlainTemplateParser(CharSequence string) {
        this.string = string;
    }

    public List<PlainNode> parse() throws TemplateParserException {
        text = new StringBuilder();
        stack = new Stack<PlainElement>();
        stack.push(new PlainElement(null, null, 0));
        while (stringIdx < string.length()) {
            Matcher matcher = patForTag.matcher(string.subSequence(stringIdx, string.length()));
            if (matcher.find()) {
                int start = matcher.start();
                text.append(string.subSequence(stringIdx, stringIdx + start));
                increateStringIndex(start);
                parseTag();
            } else {
                text.append(string.subSequence(stringIdx, string.length()));
                stringIdx = string.length();
            }
        }
        if (stack.size() != 1) throw new TemplateParserException(string.length());
        flush();
        return stack.get(0).getContent();
    }

    private void parseTag() throws TemplateParserException {
        if (string.length() - stringIdx < 4) {
            throw new TemplateParserException(stringIdx, "tag error");
        }
        Pattern patForClose = null;
        if (string.subSequence(stringIdx, stringIdx + 4).toString().equals("<!--")) {
            patForClose = patForCmtClose;
        } else if (string.length() - stringIdx > 9 && string.subSequence(stringIdx, stringIdx + 9).toString().equals("<![CDATA[")) {
            patForClose = patForCddClose;
        } else if (string.charAt(stringIdx + 1) == '?' || string.charAt(stringIdx + 1) == '!') {
            patForClose = patForTagClose;
        }
        if (patForClose != null) {
            Matcher matcher = patForClose.matcher(string.subSequence(stringIdx, string.length()));
            if (!matcher.find()) throw new TemplateParserException(string.length());
            int end = matcher.end();
            text.append(string.subSequence(stringIdx, stringIdx + end));
            stringIdx += end;
            return;
        }
        if (string.charAt(stringIdx + 1) == '/') {
            if (stack.size() <= 1) {
                throw new TemplateParserException(stringIdx, "wrong close tag");
            }
            Matcher matcher = patForTagClose.matcher(string.subSequence(stringIdx, string.length()));
            if (!matcher.find()) throw new TemplateParserException(stringIdx, "no close tag found");
            int end = matcher.end();
            String tag = string.subSequence(stringIdx + 2, stringIdx + end - 1).toString().trim();
            if (stack.isEmpty() || stack.peek().getTag() == null) {
                throw new TemplateParserException(stringIdx, "wrong close tag");
            } else if(!tag.equals(stack.peek().getTag())) {
                TemplateParserException loc = new TemplateParserException(stack.peek().getLocation());
                throw new TemplateParserException(stringIdx, "tag " + stack.peek().getTag() + "(" + loc.line + "," + loc.column + ") not close");
            }
            flush();
            stack.pop();
            stringIdx += end;
            return;
        }
        int tagLocation = stringIdx;
        Matcher matcher = patForName.matcher(string.subSequence(stringIdx + 1, string.length()));
        if (!matcher.find()) throw new TemplateParserException(stringIdx);
        int end = matcher.end() + 1;
        String tag = string.subSequence(stringIdx + 1, stringIdx + end).toString().trim();
        increateStringIndex(end);
        if (!String.valueOf(string.charAt(stringIdx)).matches("[\\s>/]")) {
            throw new TemplateParserException(stringIdx, "tag error");
        }
        flush();
        PlainElement parent = stack.peek();
        PlainElement element = new PlainElement(parent, tag, tagLocation);
        parent.getContent().add(element);
        stack.push(element);
        parseAttributes();
    }

    private void parseAttributes() throws TemplateParserException {
        for (; ;) {
            Matcher matcher = patForNotBlank.matcher(string.subSequence(stringIdx, string.length()));
            if (!matcher.find()) throw new TemplateParserException(string.length());
            int start = matcher.start();
            stringIdx += start;
            char c = string.charAt(stringIdx);
            if (c == '>') {
                stringIdx++;
                return;
            }
            if (c == '/') {
                if (string.length() < stringIdx + 2 || string.charAt(stringIdx + 1) != '>') {
                    throw new TemplateParserException(stringIdx);
                }
                stack.pop();
                stringIdx += 2;
                return;
            }
            matcher = patForName.matcher(string.subSequence(stringIdx, string.length()));
            if (!matcher.find()) throw new TemplateParserException(stringIdx);
            int end = matcher.end();
            String attr = string.subSequence(stringIdx, stringIdx + end).toString().trim();
            increateStringIndex(end);
            matcher = patForEqual.matcher(string.subSequence(stringIdx, string.length()));
            if (!matcher.find()) throw new TemplateParserException(stringIdx, "attribute '" + attr + "' has no value");
            end = matcher.end();
            increateStringIndex(end);
            matcher = patForAttrValue.matcher(string.subSequence(stringIdx, string.length()));
            if (!matcher.find()) throw new TemplateParserException(stringIdx, "attribute '" + attr + "' has no valid value");
            end = matcher.end();
            String value = string.subSequence(stringIdx, stringIdx + end).toString();
            increateStringIndex(end);
            if(value.charAt(0)=='"') {
                value = value.substring(1, value.length() - 1);
            } else {
                value = value.substring(1, value.length() - 1).replace("\"", "&quot;");
            }
            stack.peek().getAttributes().put(attr, value);
        }
    }

    private void increateStringIndex(int count) throws TemplateParserException {
        stringIdx += count;
        if (stringIdx >= string.length()) {
            throw new TemplateParserException(stringIdx, "count overflow");
        }
    }

    private void flush() {
        if (text.length() > 0) {
            stack.peek().getContent().add(new PlainText(text));
            text.setLength(0);
        }
    }

    /**
     * 模板解析异常。
     */
    public class TemplateParserException extends Exception {

        private int line = 1;
        private int column;
        private String msg;

        public TemplateParserException(int location) {
            this(location, null);
        }

        public TemplateParserException(int location, String msg) {
            this.msg = msg;
            if (location >= string.length()) {
                location = string.length() - 1;
            }
            for (; location >= 0; location--) {
                char c = string.charAt(location);
                if (c == '\n') break;
                column++;
                if (c == '\t') column += 3;
            }
            for (; location >= 0; location--) {
                if (string.charAt(location) == '\n') line++;
            }
        }

        /**
         * 获得出现解析异常的模板行号。
         * @return 行号
         */
        public int getLine() {
            return line;
        }

        /**
         * 获得出现解析异常的模板列号。
         * @return 列号
         */
        public int getColumn() {
            return column;
        }

        /**
         * 获得错误描述。
         * @return 错误描述
         */
        public String getMessage() {
            return "error at (" + line + ", " + column + ") : " + (msg==null ? "unknown" : msg);
        }
    }
}
